BroadcastChannel 握手机制的方法

BroadcastChannel 握手机制的方法实现

使用场景,dokeview tab拖出来窗口外第一次创建窗口的时候传递参数。

将这种“握手”逻辑封装成通用的工具函数,可以大大减少重复代码,并保证逻辑的健壮性。

封装了一个类型安全、支持超时控制的 WindowSync 工具库。

1. 创建工具文件 (src/utils/window-sync.ts)

这个工具包含两个核心函数:

  1. sendToChild: 父窗口调用,等待子窗口就绪后发送数据。
  2. receiveFromParent: 子窗口调用,发送就绪信号并等待数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// src/utils/window-sync.ts

// 定义消息协议
type SyncMessage<T = any> =
| { type: 'CHILD_READY'; id: string }
| { type: 'PARENT_DATA'; id: string; payload: T };

interface SyncOptions {
timeout?: number; // 超时时间,默认 5000ms
}

/**
* 【父窗口用】发送数据给新开的子窗口
* @param channelName 频道名称 (必须与子窗口一致)
* @param targetId 目标窗口/面板的唯一ID (用于过滤)
* @param data 要发送的数据
* @param options 配置项
*/
export function sendToChild<T>(
channelName: string,
targetId: string,
data: T,
options: SyncOptions = {}
): Promise<void> {
const { timeout = 5000 } = options;

return new Promise((resolve, reject) => {
const channel = new BroadcastChannel(channelName);
let timer: any;

// 清理函数
const cleanup = () => {
channel.close();
clearTimeout(timer);
};

// 1. 设置超时机制 (防止子窗口没打开,一直卡死在这里)
timer = setTimeout(() => {
cleanup();
reject(new Error(`[WindowSync] Send timeout: Child window "${targetId}" did not respond in ${timeout}ms`));
}, timeout);

// 2. 监听消息
channel.onmessage = (event: MessageEvent<SyncMessage>) => {
const { type, id } = event.data;

// 等待 "CHILD_READY" 信号,且 ID 必须匹配
if (type === 'CHILD_READY' && id === targetId) {
// 3. 发送数据
channel.postMessage({
type: 'PARENT_DATA',
id: targetId,
payload: data,
} as SyncMessage<T>);

// 4. 完成
cleanup();
resolve();
}
};
});
}

/**
* 【子窗口用】接收来自父窗口的数据
* @param channelName 频道名称
* @param myId 自己的唯一ID
* @param options 配置项
*/
export function receiveFromParent<T>(
channelName: string,
myId: string,
options: SyncOptions = {}
): Promise<T> {
const { timeout = 5000 } = options;

return new Promise((resolve, reject) => {
const channel = new BroadcastChannel(channelName);
let timer: any;

const cleanup = () => {
channel.close();
clearTimeout(timer);
};

// 1. 设置超时
timer = setTimeout(() => {
cleanup();
reject(new Error(`[WindowSync] Receive timeout: Parent did not send data for "${myId}"`));
}, timeout);

// 2. 监听父窗口发来的数据
channel.onmessage = (event: MessageEvent<SyncMessage<T>>) => {
const { type, id, payload } = event.data;

if (type === 'PARENT_DATA' && id === myId && payload !== undefined) {
cleanup();
resolve(payload);
}
};

// 3. 主动发送 "我准备好了" 信号
// 稍微延迟一点,确保父窗口的监听器已经 ready (虽然通常父窗口早就 ready 了)
setTimeout(() => {
channel.postMessage({
type: 'CHILD_READY',
id: myId,
} as SyncMessage);
}, 50);
});
}

2. 在父窗口中使用 (CustomTab.vue)

现在你的业务代码会变得非常简洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script setup lang="ts">
import { sendToChild } from '../utils/window-sync' // 引入工具

// ...

const performPopout = async () => {
const panelId = props.params.api.id
const title = props.params.api.title

// 1. 准备大对象数据
const bigData = {
panelId,
title,
userInfo: { name: 'Admin', role: 'SuperUser' },
heavyList: Array.from({ length: 1000 }, (_, i) => i)
}

// 2. 打开窗口 (只传基本 ID)
const url = `?popout=true&panelId=${panelId}&title=${encodeURIComponent(title)}`
window.open(url, '_blank')

// 3. 关闭当前面板 (UI上立即响应)
props.params.api.group.api.closePanel(props.params.api.panel)

// 4. 【关键】调用封装好的发送方法
// 不用管什么 addEventListener,一行代码搞定
try {
await sendToChild('dockview-popout', panelId, bigData)
console.log('✅ 数据发送成功!')
} catch (err) {
console.error('❌ 发送失败 (可能是窗口被拦截或关闭了):', err)
}
}
</script>

3. 在子窗口中使用 (App.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script setup lang="ts">
import { receiveFromParent } from './utils/window-sync' // 引入工具

const onReady = async (event: DockviewReadyEvent) => {
const api = event.api
const urlParams = new URLSearchParams(window.location.search)
const isPopout = urlParams.get('popout') === 'true'
const myPanelId = urlParams.get('panelId')

if (isPopout && myPanelId) {
try {
// 1. 【关键】调用封装好的接收方法
// 这里支持泛型,会有代码提示
const data = await receiveFromParent<any>('dockview-popout', myPanelId)

console.log('👶 收到父窗口投喂:', data)

// 2. 创建面板
api.addPanel({
id: myPanelId,
component: 'default',
tabComponent: 'myTab',
params: {
title: data.title,
extra: data.userInfo // 使用接收到的数据
},
title: data.title
})

} catch (err) {
console.error('❌ 获取数据失败:', err)
// 降级处理:比如显示一个错误提示或者默认空面板
}
} else {
// 主窗口逻辑...
}
}
</script>

封装后的优势

  1. 代码解耦:业务组件不需要关心 BroadcastChannel 的创建、销毁、事件监听逻辑。
  2. 资源自动回收:封装内自动调用了 channel.close(),防止开发者忘记关闭导致内存泄漏。
  3. 健壮性:增加了 timeout 超时处理。如果新窗口被浏览器拦截了,或者加载失败了,父窗口的 await 不会一直卡死。
  4. 类型安全:支持 TypeScript 泛型,接收到的 data 直接有类型提示。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器